﻿using System;
using System.Collections.Generic;
using System.Configuration;
using System.IO;
using System.Linq;
using System.Text;
using NDBGen.Configuration;
using NDBGen.Descriptors.Columns.Tokens;
using NDBGen.Plugins;

namespace NDBGen
{
    public static class Database
    {
        internal static void GenerateSchema(SchemaDatabaseOutputElement config, ParsedInput parsedInput, PluginLibraries libraries)
        {
            if (config.Files.Any())
            {
                IProjectFileManager projectFileManager;
                Boolean hasProject;
                String outputRootPath;
                var projectFile = ProjectManagement.LoadProjectFile(config, libraries, out projectFileManager, out hasProject, out outputRootPath);

                foreach (var fileConfig in config.Files)
                {
                    var generator = libraries.DatabaseScriptGenerators.FindOne(g => g.ConfigurationName == fileConfig.Generator);
                    if (generator == null) throw new ConfigurationErrorsException(String.Format("Could not find an implementation of IDataAccessCodeGenerator named {0}", fileConfig.Generator));
                    generator.Init(parsedInput);

                    var outputFile = ProjectManagement.GetOutputFile(outputRootPath, fileConfig.Path);

                    var entries = GenerateSchemaScripts(fileConfig.SchemaOutputType, null, parsedInput, generator);
                    File.WriteAllText(outputFile.FullName, entries);

                    if (hasProject)
                    {
                        projectFileManager.AddFileReference(fileConfig.Path);
                    }
                }

                if (hasProject)
                {
                    projectFileManager.Save();
                }
            }
        }

        public static String GenerateSchemaScripts(SchemaOutputType outputType, String prefix, ParsedInput parsedInput, IDatabaseScriptFileGenerator generator)
        {
            var toWrite = parsedInput.Tables.ToList();
            var result = new List<String>(toWrite.Count);
            var writtenTables = new List<Table>(toWrite.Count);
            var writtenSchemas = new List<String>();
            var lastWrittenCount = Int32.MinValue;

            while (toWrite.Any())
            {
                if (lastWrittenCount == 0)
                {
                    var writtenTableList = (
                        from table in writtenTables
                        orderby table.Schema, table.Name
                        select String.Format("{0}.{1}", table.Schema, table.Name)).ToArray();

                    var toWriteList = (
                        from table in toWrite
                        let unresolvedDependencies = (
                            from column in table.Columns
                            from descriptor in column.Descriptors
                            let fk = descriptor as ForeignKeyTokenDescriptor
                            let fkTable = fk == null ? default(TableKey) : new TableKey(fk.ForeignKeySchema, fk.ForeignKeyTable)
                            where fk != null && fkTable != table.Key && !writtenTables.Any(t => t.Key == fkTable)
                            select String.Format("{0}.{1}", fkTable.SchemaName, fkTable.TableName)
                        )
                        orderby table.Schema, table.Name
                        select String.Format("{0}.{1} ({2})", table.Schema, table.Name, String.Join(", ", unresolvedDependencies.ToArray()))).ToArray();

                    throw new Exception(String.Format("The remaining tables have dependencies that cannot be resolved with the available data.{0}Written Tables:{0}\t{1}{0}{0}Remaining Tables:{0}\t{2}",
                        Environment.NewLine,
                        String.Join(Environment.NewLine + "\t", writtenTableList),
                        String.Join(Environment.NewLine + "\t", toWriteList)));
                }

                var schemas = (
                    from table in toWrite
                    let unresolvedDependencies = (
                        from column in table.Columns
                        from descriptor in column.Descriptors
                        let fk = descriptor as ForeignKeyTokenDescriptor
                        let fkTable = fk == null ? default(TableKey) : new TableKey(fk.ForeignKeySchema, fk.ForeignKeyTable)
                        where fk != null && fkTable != table.Key && !writtenTables.Any(t => t.Key == fkTable)
                        select table
                    )
                    where !unresolvedDependencies.Any()
                    group table by table.Schema).ToArray();

                foreach (var schema in schemas)
                {
                    if (outputType == SchemaOutputType.Create && !writtenSchemas.Contains(schema.Key))
                    {
                        using (var writer = new IndentedTextWriter(new MemoryStream()))
                        {
                            generator.WriteCreateSchema(schema.Key, writer);
                            result.Add(writer.ToString());
                        }
                    }

                    foreach (var table in schema)
                    {
                        using (var writer = new IndentedTextWriter(new MemoryStream()))
                        {
                            if (outputType == SchemaOutputType.Create) generator.WriteCreateTable(table, writer);
                            else if (outputType == SchemaOutputType.Obliterate) generator.WriteDropTable(table, writer);
                            else throw new NotSupportedException("Unsupported SchemaOutputType");

                            result.Add(writer.ToString());
                        }
                    }
                }

                writtenSchemas.AddRange(schemas.Select(s => s.Key));
                writtenTables.AddRange(schemas.SelectMany(t => t));
                lastWrittenCount = schemas.SelectMany(t => t).Count();

                toWrite.RemoveAll(t =>
                {
                    var schemaTables = schemas.SelectMany(st => st);
                    return schemaTables.Contains(t);
                });
            }

            if (outputType == SchemaOutputType.Obliterate) result.Reverse();
            return String.Join(Environment.NewLine, result);
        }

        internal static void GenerateData(DataOutputElement config, ParsedInput parsedInput, PluginLibraries libraries)
        {
            if (config.Files.Any())
            {
                IProjectFileManager projectFileManager;
                Boolean hasProject;
                String outputRootPath;
                var projectFile = ProjectManagement.LoadProjectFile(config, libraries, out projectFileManager, out hasProject, out outputRootPath);

                var generator = libraries.DatabaseScriptGenerators.FindOne(g => g.ConfigurationName == config.Generator);
                if (generator == null) throw new ConfigurationErrorsException(String.Format("Could not find an implementation of IDatabaseScriptGenerator named {0}", config.Generator));
                
                var scripts = GenerateDataScripts(parsedInput, generator);

                foreach (var fileConfig in config.Files)
                {
                    var outputFile = ProjectManagement.GetOutputFile(outputRootPath, fileConfig.Path);

                    if (scripts.ContainsKey(fileConfig.Group))
                    {
                        File.WriteAllText(outputFile.FullName, scripts[fileConfig.Group]);
                    }
                    else File.WriteAllText(outputFile.FullName, String.Empty);

                    if (hasProject)
                    {
                        projectFileManager.AddFileReference(fileConfig.Path);
                    }
                }

                if (hasProject)
                {
                    projectFileManager.Save();
                }
            }
        }

        public static Dictionary<String, String> GenerateDataScripts(ParsedInput parsedInput, IDatabaseScriptFileGenerator generator)
        {
            var toWrite = (
                from dataGroup in parsedInput.ScriptedData
                from entry in dataGroup
                select new { Data = entry, Key = dataGroup.Key }).ToList();

            var written = new List<ScriptedTableData>(toWrite.Count);
            var lastWrittenCount = Int32.MinValue;
            var builders = new Dictionary<String, StringBuilder>();

            while (toWrite.Any())
            {
                if (lastWrittenCount == 0)
                {
                    var writtenTableList = (
                        from table in written
                        orderby table.TableSchemaName, table.TableName
                        select String.Format("{0}.{1}", table.TableSchemaName, table.TableName)).ToArray();

                    var toWriteList = (
                        from e in toWrite
                        let unresolvedDependencies = (
                            from c in e.Data.Columns
                            let referencedTableKey = c.ReferencedTableSchema == null ? default(TableKey) : new TableKey(c.ReferencedTableSchema, c.ReferencedTableName)
                            where referencedTableKey != default(TableKey) && referencedTableKey != e.Data.TableKey &&
                                !written.Any(w => w.TableKey == referencedTableKey)
                            select String.Format("{0}.{1}", referencedTableKey.SchemaName, referencedTableKey.TableName))
                        orderby e.Data.TableSchemaName, e.Data.TableName
                        select String.Format("{0}.{1} ({2})", e.Data.TableSchemaName, e.Data.TableName, String.Join(", ", unresolvedDependencies.ToArray()))).ToArray();

                    throw new Exception(String.Format("The remaining tables have dependencies that cannot be resolved with the available data.{0}Written Tables:{0}\t{1}{0}{0}Remaining Tables:{0}\t{2}",
                        Environment.NewLine,
                        String.Join(Environment.NewLine + "\t", writtenTableList),
                        String.Join(Environment.NewLine + "\t", toWriteList)));
                }

                var batch = (
                    from e in toWrite
                    let unresolvedDependencies = (
                        from c in e.Data.Columns
                        let referencedTableKey = c.ReferencedTableSchema == null ? default(TableKey) : new TableKey(c.ReferencedTableSchema, c.ReferencedTableName)
                        where referencedTableKey != default(TableKey) && referencedTableKey != e.Data.TableKey && 
                            !written.Any(w => w.TableKey == referencedTableKey)
                        select e)
                    where !unresolvedDependencies.Any()
                    select e).ToArray();
                
                foreach (var entry in batch)
                {
                    StringBuilder builder;
                    if (!builders.ContainsKey(entry.Key)) builders.Add(entry.Key, new StringBuilder());
                    builder = builders[entry.Key];

                    Table table;
                    {
                        var tableQuery = parsedInput.Tables.Where(t => t.Key == entry.Data.TableKey);
                        try { table = tableQuery.Single(); }
                        catch (InvalidOperationException ex)
                        {
                            if (!parsedInput.Tables.Any()) throw new InvalidDataException("No tables were defined in the schema.");
                            if (!tableQuery.Any()) throw new InvalidDataException(String.Format("The table '{0}' was defined in the data group '{1}', but was not defined in the schema.", entry.Data.TableKey, entry.Key), ex);
                            if (tableQuery.Count() > 1) throw new InvalidDataException(String.Format("The table '{0}' was defined multiple times in the schema.", entry.Data.TableKey), ex);

                            throw;
                        }
                    }

                    if (builder.Length > 0) builder.AppendLine();

                    // if the schema doesn't define as many columns as the data, there is a scripting problem
                    if (table.Columns.Count < entry.Data.Columns.Count())
                    {
                        var entryColumns = entry.Data.Columns.Select(c => c.Name);
                        var tableColumns = table.Columns.Select(c => c.Name);
                        var missingColumns = entryColumns.Where(c => !tableColumns.Contains(c));
                        throw new InvalidOperationException(String.Format("The schema definition for table '{0}.{1}' contains fewer columns than data specified in data group '{2}'. Columns missing from the schema include '{3}'.",
                            table.Schema, table.Name, entry.Key, String.Join("','", missingColumns.ToArray())));
                    }
                    else
                    // if the data doesn't provide enough data to satisfy non-null requirements, there is a scripting problem
                    {
                        var nonRequiredDescriptors = new Type[] {
                            typeof(DefaultValueTokenDescriptor),
                            typeof(AutoGeneratedTokenDescriptor)
                        };

                        // find columns that will not automatically populate if a value is not specified
                        var columns = (         
                            from c in table.Columns                            
                            let descriptors = (
                                from d in c.Descriptors
                                let nullableToken = d as NullableTokenDescriptor
                                where nonRequiredDescriptors.Contains(d.GetType())
                                    || (nullableToken != null && nullableToken.IsActive)
                                    || c.DataType == DataType.Version
                                    || c.DataType == DataType.Timestamp
                                select d)
                            select new { Column = c, Descriptors = descriptors });
                        var missingColumns = columns.Where(c => !c.Descriptors.Any() && !entry.Data.Columns.Any(ec => c.Column.Name == ec.Name));
                        if (missingColumns.Any())
                        {
                            throw new InvalidOperationException(String.Format("The data specified for the table '{0}.{1}' in data group '{2}' does not include entries for all required columns (columns that are non-nullable, do not specify a default value, and do not otherwise autogenerate a value). The missing columns are '{3}'.",
                                table.Schema, table.Name, entry.Key, String.Join("','", missingColumns.Select(mc => mc.Column.Name).ToArray())));
                        }
                    }

                    using (var writer = new IndentedTextWriter(new MemoryStream()))
                    {
                        generator.WriteDefineProcedure(entry.Key, table, entry.Data, writer);
                        builder.Append(writer.ToString());
                    }

                    builder.AppendLine();

                    using (var writer = new IndentedTextWriter(new MemoryStream()))
                    {
                        generator.WriteDefineData(entry.Key, table, entry.Data, writer);
                        builder.Append(writer.ToString());
                    }
                }

                written.AddRange(batch.Select(e => e.Data));
                toWrite.RemoveAll(e => batch.Contains(e));
                lastWrittenCount = batch.Length;
            }

            return builders.ToDictionary(e => e.Key, e => e.Value.ToString());
        }
    }
}